##############################################################################
# server.py
#
# A separate server to which a clientcan connect to using a specific port. The
# default port is 6250.
##############################################################################

import sys, socket, time, re, random


# FIXME Regular expression used to validate the "roll" command
ROLL = re.compile('^(?:roll )(\d*)d(-?[\d]+)(?:([-{0,1}+{0,1}][\d]+))?',re.S)

# Create the server and allow clients to connect with it. Note that when
# clients join, they are given a spot in the server array. When they leave,
# that spot is replaced with a 'None', preventing confusion about player
# identities. When the last player leaves, the server shuts down.
class Server(object):

	# Create the server at a certain port
    def __init__(self, port):
        self.accepting_socket = socket.socket()
        
		# Initial Message
        print(" ")
        print("PyNTT Server")
        print("Copyright 2010 - Dmitri Amariei")
        print(" ")
        print("This program is FREE SOFTWARE licensed under the GNU GPL v3.")
        print("This means this software is under NO WARRANTY.")
        print("Please see the file COPYING.txt for more details.")
        print(" ")
        
		# Try to bind the server on the specified port
        trying = True
        while trying:
            try:
                self.accepting_socket.bind( ('0.0.0.0',port) )
                trying = False
            except:
                m = "Failed to establish server. Trying again in 10 seconds."
                print(m)
                time.sleep(10)
                
        self.accepting_socket.listen( 5 )
        self.accepting_socket.settimeout( 0.01 )
        self.clients = []
        self.running = True
        print("Now listening on port "+str(port)+"!")
    
    # Parse a command string 'msg' and take action.
    def parse_command(self, msg):
        pos = msg.find("_")
        client_id = int(msg[5:pos])
        msg = msg[pos+1:]
        s = msg.strip()
        
		# Return if it is a blank msg
        if s == "": return
        
        client = self.clients[client_id]
        
        # This publicly rolls dice for the group to see
        # /roll <N>d<X>{+/-<Y>}
        #   N: number of dice
        #   X: sides of dice
        #   Y: optional, modifier
        result = ROLL.findall(s)
        if len(result) == 1 and result[0][1] != "":
            numly = 1
            if result[0][0].isdigit(): numly = int(result[0][0])
            r = 0
            hi,lo = int(result[0][1]) , 1
            if result[0][2] != "":
                lo = int(result[0][2])
            if lo > hi:
                lo,hi = hi,lo
            for i in range(numly):
                r += random.randint(lo,hi)
            dr = str(numly)+"d"
            if lo != 1:
                dr += str(lo)+","+str(hi)
            else:
                dr += str(hi)
            m = "txtP"+str(client_id)+" rolled a "+str(r)+" on "+dr+"."
            for c in self.clients:
                if c.alive:
                    c.tell(m)
            return
        
        m = "txtFailed to execute the remote command: '"+s+"'"
        client.tell(m)
    
    # The step by step actions of the server. Check for new players, handle
    # communication messages, and kill dead connections.
    def run(self):
        case = None
        
        try:
            new_socket, addr = self.accepting_socket.accept()
            new_socket.settimeout( 0.01 )
            C = client(new_socket)
            self.clients.append( C )
            
            C.tell("txtWelcome to the Server!")
            
            print( str(addr[0])+" has connected." )
            
        except socket.timeout: pass
        
        messagelist = []
        targets = []
        for i in range(len(self.clients)):
            C = self.clients[i]
            if C != None:
                C.update()
                if not C.alive:
                    self.clients[i] = None
                else:
                    targets.append(i)
                    if len(C.msgs) > 0:
                        pure = C.msgs.pop(0)
                        msg = pure[:3]+"id"+str(i)+"_"+pure[3:]
                        messagelist.append( (i,msg) )
        for m in messagelist:
            if m[1].startswith("com"):
                self.parse_command(m[1])
            else:
                for t in targets:
                    if t != m[0]:
                        self.clients[t].tell(m[1])
        
        if len(self.clients) > 0 and len(targets) == 0:
            self.running = False



# A client class that helps the server manage client information.
class client(object):
    def __init__(self, socket):
        self.socket = socket
        self.buff = ""
        self.queue = ""
        self.msgs = []
        self.alive = True
    
    # Send a message to the client. Note that this simply sets the message on a
    # buffer that will be slowly sent to the player, one kilobyte at a time.
    def tell(self, msg):
        if not self.alive: return
        
        length = str(len(msg))
        length = ("0"*(8-len(length)))+length
        self.queue += length+msg
    
    # Update the client. Send and receive data that's trapped on the queue.
    def update(self):
        if not self.alive: return
        
        data = None
        
        if len(self.queue) > 0:
            try:

				# Extract and send a kilobyte. Leave
				# the rest in the queue
                self.socket.send( self.queue[:1024] )
                self.queue = self.queue[1024:]

            except socket.timeout: pass
            except: self.kill()
        
        try:
            data = self.socket.recv(1024)
            if data == "": self.kill()
        except socket.timeout: pass
        except socket.error: self.kill()
        
        if data == None: data = ""
        
        self.buff += data   
        if not self.buff[:8].isdigit():
            self.buff = ""
        if len(self.buff) >= 8:
            size = int(self.buff[:8])
            if len(self.buff[8:]) >= size:
                m = self.buff[8:size+8]
                self.msgs.append(m)
                self.buff = self.buff[size+8:]
    
    # Kill the client's connection.
	# Set the client as 'dead' and close the socket.
    def kill(self):
        self.alive = False
        self.socket.close()
